﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Linq.Expressions;

namespace DDD {

    /// <summary>
    /// DDDで定義される全てのクラスの基底
    /// </summary>
    /// <remarks>
    /// DDDで定義される全てのクラスはこの<see cref="Object"/>を継承します。
    /// アニメーションなど全てのオブジェクトに共通する処理が実装されています。
    /// </remarks>
    public abstract class Object {

        #region Field
        int uniqueID;
        int userID;
        Dictionary<string, object> userObject;
        bool isAnimationEnabled;
        List<AnimationClip> clips;
        bool dirty;
        #endregion

        #region Constructor
        /// <summary>
        /// コンストラクター
        /// </summary>
        public Object () {
            this.uniqueID = GetHashCode ();
            this.userID = 0;
            this.userObject = new Dictionary<string, object> ();
            this.isAnimationEnabled = true;
            this.clips = new List<AnimationClip> ();
            this.dirty = true;
        }
        #endregion


        #region Property
        /// <summary>
        /// 汚染フラグ
        /// </summary>
        /// <remarks>
        /// 汚染フラグは一部のクラスで使われデータの更新が必要な事を示します。
        /// 典型的にはこのフラグがtrueの時はGPUにデータを転送する必要があります｡
        /// 通常ユーザーはこのプロパティを気にする必要はありません｡
        /// </remarks>
        public bool IsDirty {
            get { return dirty; }
        }

        /// <summary>
        /// 一意なID
        /// </summary>
        /// <remarks>
        /// オブジェクトには自動的に一意なIDが発行されます。
        /// このIDはプログラム実行中にのみ有効です。ファイルには保存されません。
        /// ファイルに保存したオブジェクトをロードするとその都度異なるIDが発行されます。
        /// </remarks>
        public int UniqueID {
            get { return uniqueID; }
        }

        /// <summary>
        /// ユーザー定義のID
        /// </summary>
        /// <remarks>
        /// ユーザーはオブジェクトに任意のIDを付加可能です。
        /// このIDの使用方法はユーザーの自由です。
        /// このIDはファイルのセーブ・ロードで値が保存されます。
        /// </remarks>
        public int UserID {
            get { return userID; }
            set { this.userID = value; }
        }

        /// <summary>
        /// ユーザー定義の任意のオブジェクト
        /// </summary>
        /// <remarks>
        /// ユーザーはオブジェクトに任意のオブジェクトを付与可能です。
        /// このデータの使用方法はユーザーの自由です。
        /// このデータは(C#言語標準の)シリアライズ可能ならファイルのセーブ・ロードで値が保存されます。
        /// </remarks>
        public Dictionary<string, object> UserObject {
            get { return userObject; }
        }

        /// <summary>
        /// アニメーションの有効・無効フラグ
        /// </summary>
        /// <remarks>
        /// このフラグがtrueの時に(設定されていれば)アニメーションによってプロパティの値が変化します。
        /// falseの時はセットされたアニメーションは無視されます。
        /// </remarks>
        public bool IsAnimationEnabled {
            get { return isAnimationEnabled; }
        }

        /// <summary>
        /// このオブジェクトにセットされたアニメーションクリップの合計
        /// </summary>
        /// <remarks>
        /// </remarks>
        public int AnimationClipCount {
            get { return clips.Count; }
        }

        /// <summary>
        /// アニメーションクリップを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// </remarks>
        public IEnumerable<AnimationClip> AnimationClips {
            get { return clips; }
        }

        /// <summary>
        /// このオブジェクトが参照する全DDDオブジェクトを列挙する列挙子
        /// </summary>
        /// <remarks>
        /// 参照にnullは含まれません。同一のオブジェクトが複数出現する可能性があります。
        /// </remarks>
        public virtual IEnumerable<Object> References {
            get {
                var objs = new List<Object> ();
                objs.AddRange (clips);
                objs.RemoveAll (x => x == null);
                return objs;
            }
        }



        #endregion

        #region Method

        /// <summary>
        /// 自分自身と自分から辿れる<see cref="Object"/>のうち指定のユーザーID(<see cref="UserID"/>)を持つものを探して返します
        /// </summary>
        /// <remarks>
        /// このオブジェクトとこのオブジェクトから到達できる全てのオブジェクトから指定のユーザーIDを持つものを１つ返します。
        /// 複数の<see cref="Object"/>が同一のユーザーIDを持つ場合、どれが帰るかは未定義です。
        /// </remarks>
        /// <param name="userID">ユーザーID</param>
        /// <returns></returns>
        public Object Find (int userID) {
            return Finds (userID).ElementAtOrDefault(0);
        }


        /// <summary>
        /// 指定のユーザーID(<see cref="UserID"/>)の<see cref="Object"/>を全て探して返します
        /// </summary>
        /// <remarks>
        /// このオブジェクトとこのオブジェクトから到達できる全てのオブジェクトから指定のユーザーIDを持つものを全て返します。
        /// </remarks>
        /// <param name="userID">ユーザーID</param>
        /// <returns></returns>
        public Object[] Finds (int userID) {
            var founds = new List<Object> ();
            if (this.userID == userID) {
                founds.Add(this);
            }
            foreach (var obj in References) {
                founds.AddRange (obj.Finds (userID));
            }
            return founds.ToArray();
        }

        /// <summary>
        /// 汚染フラグをセットします
        /// </summary>
        /// <remarks>
        /// このフラグをtrueにするとGPUに再度データが転送されます｡
        /// CPU側とGPU側のデータの同期は全て自動で行われるので､
        /// 通常ユーザーが汚染フラグを自分で変更する必要はありません｡
        /// </remarks>
        /// <param name="dirty">汚染フラグ</param>
        public void SetDirtyFlag (bool dirty) {
            this.dirty = dirty;
        }

        /// <summary>
        /// ユーザーオブジェクトをセットします
        /// </summary>
        /// <remarks>
        /// ユーザーオブジェクトの使い方はユーザーの任意です｡
        /// </remarks>
        /// <param name="name">名前</param>
        /// <param name="obj">オブジェクト</param>
        public void SetUserObject (string name, object obj) {
            if (name == null || name == "") {
                throw new ArgumentException ("Name is null or empty");
            }

            this.userObject[name] = obj;
        }

        /// <summary>
        /// ユーザーオブジェクトを取得します
        /// </summary>
        /// <remarks>
        /// 
        /// </remarks>
        /// <param name="name">名前</param>
        /// <returns></returns>
        public object GetUserObject (string name) {
            if (name == null || name == "") {
                throw new ArgumentException ("Name is null or empty");
            }

            return userObject[name];
        }



        /// <summary>
        /// アニメーションします
        /// </summary>
        /// <remarks>
        /// <see cref="IsAnimationEnabled"/>がtrueで、かつアニメーションクリップが設定されている時
        /// そのクリップでプロパティの値を書き換えます。
        /// 現在アニメーション可能なのはfloat, int, IAnimatable{float}, IAnimatable{int}の４つのみです。
        /// 実装めんどくさい・・・
        /// </remarks>
        /// <param name="worldTime">時刻(ワールド時間)</param>
        public void Animate (int worldTime) {
            if (!isAnimationEnabled) {
                return;
            }

            var value = new float[16];

            foreach (var clip in clips) {
                var localTime = clip.GetLocalTime (worldTime);

                foreach (var track in clip.Tracks) {
                    var name = track.TargetProperty;
                    var propinfo = this.GetType ().GetProperty (name);

                    var sf = propinfo.GetValue (this, null) as float?;
                    if (sf != null) {
                        track.Sample (localTime, value);
                        propinfo.SetValue (this, value[0], null);
                    }

                    var si = propinfo.GetValue (this, null) as int?;
                    if (si != null) {
                        track.Sample (localTime, value);
                        propinfo.SetValue (this, (int)value[0], null);
                    }

                    var vf = propinfo.GetValue (this, null) as IAnimatable<float>;
                    if (vf != null) {

                        track.Sample (localTime, value);
                        for (int i = 0; i < vf.ComponentCount; i++) {
                            vf[i] = value[i];
                        }
                        propinfo.SetValue (this, vf, null);
                    }

                    var vi = propinfo.GetValue (this, null) as IAnimatable<int>;
                    if (vi != null) {
                        track.Sample (localTime, value);
                        for (int i = 0; i < vi.ComponentCount; i++) {
                            vi[i] = (int)value[i];
                        }
                        propinfo.SetValue (this, vi, null);
                    }

                }
            }
        }







        /// <summary>
        /// アニメーションを有効化します
        /// </summary>
        /// <remarks>
        /// アニメーションの有効・無効を切り替えます。
        /// </remarks>
        /// <param name="enable">有効･無効</param>
        public void SetAnimationEnable (bool enable) {
            this.isAnimationEnabled = enable;
        }


        /// <summary>
        /// アニメーションを追加します
        /// </summary>
        /// <remarks>
        /// このオブジェクトにアニメーション(クリップ)を追加します。
        /// <see cref="Animate"/>メソッドを呼ぶとクリップに保存されたトラックのキーフレームから値をサンプリングし、
        /// 指定のプロパティの値を書き換えます。
        /// </remarks>
        /// <param name="clip">追加したいアニメーションクリップ</param>
        public void AddAnimation (AnimationClip clip) {
            if (clip == null) {
                throw new ArgumentNullException ("Clip is null");
            }
            if (clips.Contains (clip)) {
                throw new ArgumentException ("Clip is already registered");
            }

            this.clips.Add (clip);
        }

        /// <summary>
        /// アニメーションを削除します
        /// </summary>
        /// <remarks>
        /// 指定のアニメーションクリップを削除します。
        /// </remarks>
        /// <param name="clip">削除したいクリップ</param>
        public bool RemoveAnimation (AnimationClip clip) {
            return this.clips.Remove (clip);
        }

        /// <summary>
        /// プロパティの名前を取得します
        /// </summary>
        /// <remarks>
        /// プロパティをプロパティの名前に変換します。
        /// 呼び出し例：<code>var name = Object.GetName({}=>x.SomeProperty)</code>
        /// </remarks>
        /// <typeparam name="T">式木の型</typeparam>
        /// <param name="e">式木</param>
        /// <returns>プロパティの名前</returns>
        public static string GetName<T> (Expression<Func<T>> e) {
            var member = (MemberExpression)e.Body;
            return member.Member.Name;
        }


        /// <summary>
        /// 毎フレームのレイズ処理を行います
        /// </summary>
        /// <remarks>
        /// このメソッドを呼ぶと(もしあれば)指定された時刻にセットされたアニメーションイベントが呼び出されます。
        /// アニメーションイベントの実態は指定の時刻に呼び出されるようセットされたコールバック関数です。
        /// </remarks>
        /// <param name="start">開始時刻(ワールド時間)</param>
        /// <param name="end">終了時刻(ワールド時間)</param>
        public void Raise (int start, int end) {
            foreach (var clip in clips) {
                var posStart = clip.GetLocalTime (start);
                var posEnd = clip.GetLocalTime (end);

                var events = from ev in clip.Events
                             where ev.Time >= posStart && ev.Time < posEnd
                             select ev;
                foreach (var ev in events) {
                    ev.Handler (null, ev.Args);
                }
            }
        }

        /// <summary>
        /// 現在のオブジェクトの状態を表す文字列を返します。
        /// </summary>
        /// <returns>現在のオブジェクトを説明する文字列</returns>
        public override string ToString () {
            return string.Format ("Object: clips={0}", AnimationClipCount);
        }
        #endregion

    }
}
